{% extends "console/layout.html" %}
{% block title %}Call Record Detail · {{site_name|default('RustPBX')}}{% endblock %}
{% block content %}
<div class="p-6">
    <div class="mx-auto max-w-6xl space-y-6" x-data="callRecordDetail({{ call_data | escape }})">
        <header class="flex flex-col gap-4 border-b border-slate-200 pb-4 md:flex-row md:items-end md:justify-between">
            <div class="space-y-2">
                <div class="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-sky-600">
                    <a :href="backUrl"
                        class="inline-flex items-center gap-1 text-slate-500 transition hover:text-sky-600">
                        <svg class="h-3 w-3" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6">
                            <path stroke-linecap="round" stroke-linejoin="round" d="M12 5l-5 5 5 5" />
                        </svg>
                        Back
                    </a>
                    <span>/</span>
                    <span x-text="record.display_id || record.id"></span>
                </div>
                <h1 class="text-3xl font-semibold text-slate-900">
                    <span>#</span>
                    <span x-text="record.display_id || record.id"></span>
                </h1>
                <div class="flex items-center gap-2 text-xs text-slate-500">
                    <span>Call-Id</span>
                    <span class="font-mono" :title="record.call_id" x-text="record.call_id || '—'"></span>
                    <template x-if="record.call_id">
                        <div class="flex items-center gap-1">
                            <button type="button"
                                class="inline-flex h-5 w-5 items-center justify-center rounded border border-slate-200 text-slate-400 transition hover:border-slate-300 hover:text-slate-600"
                                title="Copy call ID" aria-label="Copy call ID" @click="copyCallId(record.call_id)">
                                <svg class="h-3 w-3" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                    stroke-width="1.6">
                                    <path stroke-linecap="round" stroke-linejoin="round" d="M6 6h8v8H6z" />
                                    <path stroke-linecap="round" stroke-linejoin="round" d="M4 4h8" />
                                </svg>
                            </button>
                            <span class="text-[10px] font-semibold text-emerald-600"
                                x-show="copiedCallId === record.call_id" x-transition.opacity>Copied</span>
                        </div>
                    </template>
                </div>
                <p class="text-sm text-slate-500" x-text="formatDateTime(record.started_at)"></p>
                <div class="flex flex-wrap items-center gap-2 text-xs font-semibold">
                    <span class="inline-flex items-center gap-1 rounded-full px-3 py-1"
                        :class="statusClasses(record.status)">
                        <span class="h-1.5 w-1.5 rounded-full" :class="statusDot(record.status)"></span>
                        <span x-text="statusLabel(record.status)"></span>
                    </span>
                    <span class="inline-flex items-center gap-1 rounded-full px-3 py-1"
                        :class="directionClasses(record.direction)">
                        <svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                            stroke-width="1.6">
                            <path stroke-linecap="round" stroke-linejoin="round" d="M5 10h10M10 5l5 5-5 5" />
                        </svg>
                        <span x-text="directionLabel(record.direction)"></span>
                    </span>
                    <span
                        class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-3 py-1 text-[11px] font-semibold text-slate-600">
                        <svg class="h-3 w-3" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6">
                            <path stroke-linecap="round" stroke-linejoin="round" d="M4 4h12v12H4z" />
                            <path stroke-linecap="round" stroke-linejoin="round" d="M8 4v12M12 8h4" />
                        </svg>
                        <span x-text="record.sip_gateway || '—'"></span>
                    </span>
                </div>
            </div>
            <div class="flex flex-wrap items-center gap-3 text-sm">
                <a :href="actions.download_metadata || '#'"
                    class="inline-flex items-center gap-2 rounded-lg border border-slate-200 px-4 py-2 font-semibold text-slate-600 transition hover:border-sky-300 hover:text-sky-700">
                    <svg class="h-4 w-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M4 12l6 6 6-6" />
                        <path stroke-linecap="round" stroke-linejoin="round" d="M10 18V4" />
                    </svg>
                    Metadata JSON
                </a>
                <a :href="actions.download_sip_flow || '#'"
                    class="inline-flex items-center gap-2 rounded-lg border border-slate-200 px-4 py-2 font-semibold text-slate-600 transition hover:border-sky-300 hover:text-sky-700">
                    <svg class="h-4 w-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M4 4h12v12H4z" />
                        <path stroke-linecap="round" stroke-linejoin="round" d="M6 8h8M6 12h4" />
                    </svg>
                    SIP trace JSON
                </a>
            </div>
        </header>

        <section class="grid gap-3 sm:grid-cols-4">
            <div class="rounded-xl bg-white p-3 shadow-sm ring-1 ring-black/5">
                <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">Duration</div>
                <div class="mt-1 text-xl font-semibold text-slate-900" x-text="formatDuration(record.duration_secs)">
                </div>
                <p class="mt-1 text-xs text-slate-500" x-text="durationHint"></p>
            </div>
            <div class="rounded-xl bg-white p-3 shadow-sm ring-1 ring-black/5">
                <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">Quality (MOS)</div>
                <div class="mt-1 text-xl font-semibold" :class="qualityTone(record.quality?.mos)"
                    x-text="formatNumber(record.quality?.mos)"></div>
                <p class="mt-1 text-xs text-slate-500">Packet loss <span class="font-semibold"
                        x-text="formatNumber(record.quality?.packet_loss) + '%'"></span> · Jitter <span
                        class="font-semibold" x-text="formatNumber(record.quality?.jitter_ms) + ' ms'"></span></p>
            </div>
            <div class="rounded-xl bg-white p-3 shadow-sm ring-1 ring-black/5">
                <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">Transcript</div>
                <div class="mt-1 flex items-center gap-1.5 text-xl font-semibold"
                    :class="transcript.available || transcriptVisible ? 'text-emerald-600' : 'text-slate-400'">
                    <svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M5 12l5 5L20 7" />
                    </svg>
                    <span x-text="transcript.available || transcriptVisible ? 'Ready' : 'Pending'"></span>
                </div>
                <p class="mt-1 text-xs text-slate-500" x-text="transcriptGeneratedLabel()"></p>
            </div>
            <div class="rounded-xl bg-white p-3 shadow-sm ring-1 ring-black/5">
                <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">Billing</div>
                <div class="mt-1 flex items-center justify-between gap-1.5">
                    <span class="text-xl font-semibold text-slate-900"
                        x-text="formatBillingAmount(record.billing)"></span>
                    <span class="text-xs font-semibold uppercase text-slate-400"
                        x-text="(record.billing && record.billing.currency) ? record.billing.currency : ''"></span>
                </div>
                <div class="mt-1 inline-flex items-center gap-1.5">
                    <span class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-semibold"
                        :class="billingStatusClasses(record.billing?.status)">
                        <span x-text="billingStatusLabel(record.billing?.status)"></span>
                    </span>
                    <span class="text-[11px] text-slate-500" x-text="formatBillingMinutes(record.billing)"></span>
                </div>
                <p class="mt-1 text-xs text-slate-500"
                    x-text="billing?.summary?.rate_per_minute !== undefined ? ('Rate ' + formatCurrency(billing.summary.rate_per_minute, billing.summary.currency)) : 'Real-time charge capture.'">
                </p>
            </div>
        </section>

        <section class="space-y-4">
            <div class="rounded-xl bg-white p-2 shadow-sm ring-1 ring-black/5">
                <nav class="inline-flex w-full flex-wrap gap-1 rounded-lg border border-slate-200 bg-slate-50 p-1 text-xs font-semibold text-slate-600"
                    role="tablist" aria-label="Call record detail tabs">
                    <button type="button" class="flex-1 rounded-md px-3 py-1.5 text-left transition sm:flex-none"
                        :class="detailTab === 'overview' ? 'bg-white text-sky-700 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
                        :aria-selected="detailTab === 'overview'" aria-controls="call-record-tab-overview"
                        @click="detailTab = 'overview'">
                        <div class="flex flex-col">
                            <span class="text-[13px]">Overview</span>
                            <span class="text-[11px] font-normal text-slate-400">Participants, media, transcript</span>
                        </div>
                    </button>
                    <button type="button" class="flex-1 rounded-md px-3 py-1.5 text-left transition sm:flex-none"
                        :class="detailTab === 'diagnostics' ? 'bg-white text-sky-700 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
                        :aria-selected="detailTab === 'diagnostics'" aria-controls="call-record-tab-diagnostics"
                        @click="detailTab = 'diagnostics'">
                        <div class="flex flex-col">
                            <span class="text-[13px]">Diagnostics</span>
                            <span class="text-[11px] font-normal text-slate-400">SIP flow, metrics</span>
                        </div>
                    </button>
                    <button type="button" class="flex-1 rounded-md px-3 py-1.5 text-left transition sm:flex-none"
                        :class="detailTab === 'notes' ? 'bg-white text-sky-700 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
                        :aria-selected="detailTab === 'notes'" aria-controls="call-record-tab-notes"
                        @click="detailTab = 'notes'">
                        <div class="flex flex-col">
                            <span class="text-[13px]">Notes &amp; Billing</span>
                            <span class="text-[11px] font-normal text-slate-400">Tags, notes, billing</span>
                        </div>
                    </button>
                </nav>
            </div>

            <div x-show="detailTab === 'overview'" x-cloak x-transition.opacity class="space-y-4"
                id="call-record-tab-overview" role="tabpanel" tabindex="0">

                <div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
                    <div class="mt-6 grid gap-3 sm:grid-cols-2">
                        <template x-for="participant in participants" :key="participant.role">
                            <div class="rounded-lg border border-slate-200 p-3 text-sm text-slate-600">
                                <div class="flex items-center justify-between text-xs text-slate-400">
                                    <span x-text="participant.label"></span>
                                    <span class="rounded-full bg-slate-100 px-2 py-0.5 font-semibold text-slate-600"
                                        x-text="participant.network"></span>
                                </div>
                                <div class="mt-1 text-sm font-semibold text-slate-900" x-text="participant.name || '—'">
                                </div>
                                <template x-if="participant.role === 'caller' || participant.role === 'callee'">
                                    <div
                                        class="mt-3 space-y-1 rounded border border-dashed border-slate-200 px-3 py-2 text-[11px] text-slate-500">
                                        <div class="flex items-center justify-between">
                                            <span class="font-semibold text-slate-600">Original</span>
                                            <span class="font-mono"
                                                x-text="(rewrite[participant.role] && rewrite[participant.role].original) || '—'"></span>
                                        </div>
                                        <div class="flex items-center justify-between">
                                            <span class="font-semibold text-slate-600">Rewrite</span>
                                            <span class="font-mono"
                                                x-text="(rewrite[participant.role] && rewrite[participant.role].final) || '—'"></span>
                                        </div>
                                    </div>
                                </template>
                            </div>
                        </template>
                    </div>
                </div>
                <div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
                    <h2 class="text-base font-semibold text-slate-900">Recording & participants</h2>
                    <p class="mt-1 text-xs text-slate-500">Validate caller identification, agent mapping, and playback
                        the original media stream.</p>
                    <template x-if="record.recording && record.recording.url">
                        <audio class="mt-4 w-full" controls preload="none" :src="record.recording.url"></audio>
                    </template>
                    <template x-if="!record.recording || !record.recording.url">
                        <div
                            class="mt-4 rounded-lg border border-dashed border-slate-200 px-4 py-6 text-center text-xs text-slate-400">
                            No media recording stored for this session.
                        </div>
                    </template>
                    <div class="mt-6 rounded-lg border border-slate-200 bg-slate-50 p-4">
                        <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                            <div>
                                <h3 class="text-sm font-semibold text-slate-900">Transcript</h3>
                                <p class="text-xs text-slate-500">Generate speech-to-text to accelerate QA and
                                    analytics.</p>
                                <div class="mt-2 flex items-center gap-2 text-xs font-semibold"
                                    :class="transcriptStatusTone(transcriptStatus)">
                                    <svg x-cloak
                                        x-show="transcriptStatusIcon(transcriptStatus, record.has_transcript, transcriptVisible) === 'ready'"
                                        class="h-4 w-4" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                        stroke-width="1.6">
                                        <path stroke-linecap="round" stroke-linejoin="round" d="M5 10l4 4L15 6" />
                                    </svg>
                                    <svg x-cloak
                                        x-show="transcriptStatusIcon(transcriptStatus, record.has_transcript, transcriptVisible) === 'processing'"
                                        class="h-4 w-4 animate-spin" viewBox="0 0 20 20" fill="none"
                                        stroke="currentColor" stroke-width="1.6">
                                        <circle class="opacity-25" cx="10" cy="10" r="8" stroke-width="1.5"></circle>
                                        <path class="opacity-75" fill="currentColor"
                                            d="M10 2a8 8 0 0 1 8 8h-2a6 6 0 0 0-6-6V2Z" />
                                    </svg>
                                    <svg x-cloak
                                        x-show="transcriptStatusIcon(transcriptStatus, record.has_transcript, transcriptVisible) === 'failed'"
                                        class="h-4 w-4" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                        stroke-width="1.6">
                                        <path stroke-linecap="round" stroke-linejoin="round" d="M6 6l8 8M6 14L14 6" />
                                    </svg>
                                    <svg x-cloak
                                        x-show="transcriptStatusIcon(transcriptStatus, record.has_transcript, transcriptVisible) === 'pending'"
                                        class="h-4 w-4" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                        stroke-width="1.6">
                                        <path stroke-linecap="round" stroke-linejoin="round" d="M10 4v12m6-6H4" />
                                    </svg>
                                    <span x-text="transcriptStatusLabel(transcriptStatus)"></span>
                                </div>
                                <p class="text-[11px] text-slate-400" x-text="transcriptStatusDescription()"></p>
                            </div>
                            <div class="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-end sm:justify-end">
                                <div class="w-full sm:w-48">
                                    <label
                                        class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-400">
                                        Language
                                    </label>
                                    <div class="relative">
                                        <select
                                            class="w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200 disabled:cursor-not-allowed disabled:opacity-60"
                                            x-model="selectedLanguage"
                                            @change="updateSelectedLanguage($event.target.value)"
                                            :disabled="asrProcessing || transcriptLoading">
                                            <template x-for="option in languageOptions" :key="option.value">
                                                <option :value="option.value" x-text="option.label"></option>
                                            </template>
                                        </select>
                                    </div>
                                </div>
                                <button type="button"
                                    class="inline-flex items-center gap-2 rounded-lg bg-sky-600 px-3 py-2 text-sm font-semibold text-white transition hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60"
                                    :disabled="asrProcessing || transcriptLoading || !canRequestTranscript()"
                                    :aria-disabled="asrProcessing || transcriptLoading || !canRequestTranscript()"
                                    :title="!canRequestTranscript() ? (transcriptCapabilityIssues()[0] || 'SenseVoice CLI is not ready.') : ''"
                                    @click="requestTranscript(transcriptVisible && transcriptStatus !== 'processing')">
                                    <svg x-cloak x-show="!(asrProcessing || transcriptLoading)" class="h-4 w-4"
                                        viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8">
                                        <path stroke-linecap="round" stroke-linejoin="round" d="M10 4v12m6-6H4" />
                                    </svg>
                                    <svg x-cloak x-show="asrProcessing || transcriptLoading"
                                        class="h-4 w-4 animate-spin" viewBox="0 0 20 20" fill="none"
                                        stroke="currentColor" stroke-width="1.5">
                                        <circle class="opacity-25" cx="10" cy="10" r="7" stroke-width="1.5"></circle>
                                        <path class="opacity-75" fill="currentColor"
                                            d="M10 3a7 7 0 0 1 7 7h-1.8a5.2 5.2 0 0 0-5.2-5.2V3Z" />
                                    </svg>
                                    <span x-text="transcriptButtonLabel()"></span>
                                </button>
                            </div>
                        </div>
                        <template x-if="transcriptError">
                            <div class="mt-3 rounded border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-600"
                                x-text="transcriptError"></div>
                        </template>
                        <template x-if="transcriptVisible">
                            <div class="mt-4 space-y-3 text-sm text-slate-600">
                                <div class="flex flex-wrap items-center gap-2 text-xs text-slate-400">
                                    <span>Language</span>
                                    <span class="rounded-full bg-slate-100 px-2 py-0.5 font-semibold text-slate-600"
                                        x-text="languageLabel(selectedLanguage)"></span>
                                    <span x-show="formatGeneratedTimestamp(transcript.generated_at)"
                                        x-text="formatGeneratedTimestamp(transcript.generated_at)"></span>
                                </div>
                                <template x-if="transcriptTimeline().length">
                                    <div class="max-h-96 space-y-3 overflow-y-auto pr-2">
                                        <template x-for="item in transcriptTimeline()" :key="item.key">
                                            <div class="flex" :class="transcriptAlignmentClass(item.side)">
                                                <div class="flex max-w-xl flex-col gap-1 rounded-lg border px-3 py-2 text-sm text-slate-700 shadow-sm"
                                                    :class="transcriptCardTone(item.side)">
                                                    <div
                                                        class="flex items-center justify-between text-[11px] text-slate-500">
                                                        <span class="font-semibold text-slate-600"
                                                            x-text="item.label"></span>
                                                        <span class="font-mono"
                                                            x-text="formatSegmentRange(item.segment.start, item.segment.end)"></span>
                                                    </div>
                                                    <p class="leading-relaxed text-slate-800"
                                                        x-text="item.segment.text"></p>
                                                </div>
                                            </div>
                                        </template>
                                    </div>
                                </template>
                                <template x-if="!transcriptTimeline().length">
                                    <div
                                        class="rounded border border-dashed border-slate-200 p-4 text-center text-xs text-slate-400">
                                        Transcript generated without segment breakdown.
                                    </div>
                                </template>
                            </div>
                        </template>
                        <template x-if="!transcriptVisible">
                            <div class="mt-4 space-y-2 text-xs text-slate-500">
                                <template x-if="transcriptLoading">
                                    <div class="rounded border border-slate-200 bg-white px-3 py-2">Loading transcript…
                                    </div>
                                </template>
                                <template x-if="asrProcessing">
                                    <div class="rounded border border-slate-200 bg-white px-3 py-2">Submitting recording
                                        to sensevoice-cli ...</div>
                                </template>
                                <template x-if="!transcriptLoading && !asrProcessing && canRequestTranscript()">
                                    <div
                                        class="rounded border border-dashed border-slate-200 px-4 py-3 text-center text-xs text-slate-400">
                                        No transcript generated yet.
                                    </div>
                                </template>
                                <template x-if="!canRequestTranscript()">
                                    <div
                                        class="space-y-1 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-[11px] text-amber-700">
                                        <p class="font-semibold">SenseVoice CLI is not ready.</p>
                                        <template x-for="reason in transcriptCapabilityIssues()" :key="reason">
                                            <p x-text="reason"></p>
                                        </template>
                                    </div>
                                </template>
                            </div>
                        </template>
                    </div>
                </div>

            </div>

            <div x-show="detailTab === 'diagnostics'" x-cloak x-transition.opacity class="space-y-4"
                id="call-record-tab-diagnostics" role="tabpanel" tabindex="0">
                <div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
                    <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
                        <div>
                            <h2 class="text-base font-semibold text-slate-900">Interaction insights</h2>
                            <p class="text-xs text-slate-500">Inspect SIP signalling timeline to speed up
                                troubleshooting.</p>
                        </div>
                    </div>
                    <div>
                        <p class="text-xs text-slate-500">Compare caller, PBX, and callee lanes. Click a row to inspect
                            the raw SIP payload.</p>
                        <div class="mt-4 flex flex-col gap-4 lg:flex-row">
                            <div class="lg:w-7/12 lg:min-w-0 space-y-3">
                                <div class="rounded-xl bg-white shadow-sm ring-1 ring-black/5">
                                    <div class="max-h-[520px] overflow-y-auto overflow-x-hidden">
                                        <table class="min-w-full divide-y divide-slate-200 text-sm">
                                            <thead
                                                class="bg-slate-50 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
                                                <tr>
                                                    <template x-for="column in sipFlowColumns"
                                                        :key="'head-' + column.id">
                                                        <th class="px-3 py-2 text-left"
                                                            :class="column.id === 'pbx' ? 'text-center' : (column.id === 'callee' ? 'text-right' : 'text-left')"
                                                            x-text="column.label"></th>
                                                    </template>
                                                </tr>
                                            </thead>
                                            <tbody class="bg-white">
                                                <template x-if="sipFlowEntries.length">
                                                    <template x-for="entry in sipFlowEntries"
                                                        :key="'sip-' + entry.sequence">
                                                        <tr class="cursor-pointer transition" :class="selectedSipEntry && selectedSipEntry.sequence === entry.sequence
                                                                ? 'bg-sky-50 ring-1 ring-inset ring-sky-300'
                                                                : 'hover:bg-slate-50'" @click="selectSipEntry(entry)">
                                                            <template x-for="column in sipFlowColumns"
                                                                :key="column.id + '-' + entry.sequence">
                                                                <td
                                                                    class="align-top px-3 py-2 whitespace-normal break-words">
                                                                    <template x-if="column.id === 'time'">
                                                                        <div class="space-y-0.5 text-xs text-slate-600">
                                                                            <div class="font-mono text-[12px] font-semibold text-slate-700"
                                                                                x-text="formatRelativeToStart(entry)">
                                                                            </div>
                                                                            <div class="text-[10px] font-mono text-slate-400"
                                                                                x-text="entry.offset || ''"></div>
                                                                            <div class="text-[10px] uppercase tracking-wide text-slate-400"
                                                                                x-text="(entry.flow_direction || entry.direction || '') ? (entry.flow_direction || entry.direction || '').toUpperCase() : ''">
                                                                            </div>
                                                                        </div>
                                                                    </template>
                                                                    <template x-if="column.id !== 'time'">
                                                                        <div
                                                                            class="space-y-0.5 text-[11px] text-slate-600">
                                                                            <template
                                                                                x-if="column.id === (entry.lane_display || entry.lane_from)">
                                                                                <div class="flex items-start gap-1.5"
                                                                                    :class="laneContentClass(entry, column.id)">
                                                                                    <template
                                                                                        x-if="laneCell(entry, column.id) === 'from' && arrowPlacement(entry, column.id, 'from') === 'left'">
                                                                                        <span
                                                                                            class="text-base leading-none text-slate-400"
                                                                                            x-text="cellArrow(entry, column.id)"></span>
                                                                                    </template>
                                                                                    <template
                                                                                        x-if="laneCell(entry, column.id) === 'to' && arrowPlacement(entry, column.id, 'to') === 'left'">
                                                                                        <span
                                                                                            class="text-base leading-none text-slate-400"
                                                                                            x-text="cellArrow(entry, column.id)"></span>
                                                                                    </template>
                                                                                    <div class="space-y-1"
                                                                                        :class="laneCell(entry, column.id) === 'to' ? 'text-right' : 'text-left'">
                                                                                        <span
                                                                                            class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-mono text-[11px] font-semibold"
                                                                                            :class="laneCell(entry, column.id) === 'to' ? 'bg-slate-100 text-slate-600' : 'bg-slate-200 text-slate-700'">
                                                                                            <span
                                                                                                x-text="entry.method || entry.status_code || '—'"></span>
                                                                                            <span
                                                                                                x-show="entry.status_code && entry.method && entry.method !== String(entry.status_code)"
                                                                                                class="text-emerald-600"
                                                                                                x-text="entry.status_code"></span>
                                                                                        </span>
                                                                                        <div class="leading-snug break-words"
                                                                                            x-text="formatSummary(entry.summary)">
                                                                                        </div>
                                                                                    </div>
                                                                                    <template
                                                                                        x-if="laneCell(entry, column.id) === 'from' && arrowPlacement(entry, column.id, 'from') === 'right'">
                                                                                        <span
                                                                                            class="text-base leading-none text-slate-400"
                                                                                            x-text="cellArrow(entry, column.id)"></span>
                                                                                    </template>
                                                                                    <template
                                                                                        x-if="laneCell(entry, column.id) === 'to' && arrowPlacement(entry, column.id, 'to') === 'right'">
                                                                                        <span
                                                                                            class="text-base leading-none text-slate-400"
                                                                                            x-text="cellArrow(entry, column.id)"></span>
                                                                                    </template>
                                                                                </div>
                                                                            </template>
                                                                            <template
                                                                                x-if="laneCell(entry, column.id) === 'self'">
                                                                                <div
                                                                                    class="mx-auto h-1.5 w-1.5 rounded-full bg-slate-400">
                                                                                </div>
                                                                            </template>
                                                                        </div>
                                                                    </template>
                                                                </td>
                                                            </template>
                                                        </tr>
                                                    </template>
                                                </template>
                                                <template x-if="!sipFlowEntries.length">
                                                    <tr>
                                                        <td class="px-4 py-6 text-center text-xs text-slate-400"
                                                            :colspan="sipFlowColumns.length">
                                                            No SIP trace captured for this session.
                                                        </td>
                                                    </tr>
                                                </template>
                                            </tbody>
                                        </table>
                                    </div>
                                </div>
                            </div>
                            <div class="flex-1 min-w-0">
                                <div class="h-full rounded-xl bg-white p-4 shadow-sm ring-1 ring-black/5">
                                    <template x-if="selectedSipEntry">
                                        <div class="flex h-full flex-col">
                                            <div
                                                class="flex flex-col gap-2 border-b border-slate-200 pb-3 text-sm text-slate-600">
                                                <div class="flex flex-wrap items-center gap-2 text-xs text-slate-500">
                                                    <span
                                                        class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-3 py-1 font-semibold text-slate-700">
                                                        <span
                                                            x-text="selectedSipEntry.method || selectedSipEntry.status_code || 'Message'"></span>
                                                        <span
                                                            x-show="selectedSipEntry.status_code && selectedSipEntry.method && selectedSipEntry.method !== String(selectedSipEntry.status_code)"
                                                            class="text-emerald-600"
                                                            x-text="selectedSipEntry.status_code"></span>
                                                    </span>
                                                    <span class="font-mono text-[13px] text-slate-700"
                                                        x-text="formatDateTimeWithMs(selectedSipEntry.timestamp)"></span>
                                                    <span class="font-mono text-[11px] text-slate-400"
                                                        x-text="formatRelativeToStart(selectedSipEntry)"></span>
                                                </div>
                                                <div
                                                    class="flex flex-wrap items-center gap-2 text-[11px] text-slate-500">
                                                    <span x-text="selectedSipEntry.from_label"></span>
                                                    <span class="text-base leading-none text-slate-300"
                                                        x-text="selectedSipEntry.arrow === 'right' ? '→' : '←'"></span>
                                                    <span x-text="selectedSipEntry.to_label"></span>
                                                    <span x-show="selectedSipEntry.leg_role_label"
                                                        class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 font-semibold text-slate-600"
                                                        x-text="selectedSipEntry.leg_role_label"></span>
                                                    <span x-show="selectedSipEntry.leg_label"
                                                        class="inline-flex items-center gap-1 rounded-full border border-slate-200 px-2 py-0.5 font-mono text-[10px] text-slate-500"
                                                        x-text="selectedSipEntry.leg_label"></span>
                                                </div>
                                                <div class="text-sm font-semibold text-slate-700"
                                                    x-text="selectedSipEntry.summary"></div>
                                            </div>
                                            <div class="mt-3 flex-1 overflow-auto">
                                                <pre
                                                    class="h-96 max-w-full overflow-auto whitespace-pre rounded bg-slate-950/90 p-3 text-[11px] leading-relaxed text-emerald-100"><code x-text="selectedSipEntry.raw || 'No raw payload captured.'"></code></pre>
                                            </div>
                                        </div>
                                    </template>
                                    <template x-if="!selectedSipEntry">
                                        <div class="flex h-full items-center justify-center text-xs text-slate-400">
                                            Select a SIP message to inspect its raw payload.
                                        </div>
                                    </template>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

            <div x-show="detailTab === 'notes'" x-cloak x-transition.opacity class="space-y-4"
                id="call-record-tab-notes" role="tabpanel" tabindex="0">
                <div class="grid gap-4 lg:grid-cols-2">
                    <div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
                        <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                            <div>
                                <h2 class="text-base font-semibold text-slate-900">Tags</h2>
                                <p class="text-xs text-slate-500">Label conversations for follow-up workflows.</p>
                            </div>
                            <div class="flex items-center gap-2">
                                <input type="text" x-model.trim="tagInput"
                                    class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200"
                                    placeholder="Add tag" @keyup.enter.prevent="addTag">
                                <button type="button"
                                    class="inline-flex items-center rounded-lg bg-sky-600 px-3 py-2 text-sm font-semibold text-white transition hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60"
                                    :disabled="tagSaving || !tagInput" @click="addTag">Add</button>
                            </div>
                        </div>
                        <div class="mt-3 flex flex-wrap gap-2">
                            <template x-for="(tag, index) in (record.tags || [])" :key="tag + index">
                                <span
                                    class="inline-flex items-center gap-2 rounded-full bg-slate-100 px-3 py-1 text-sm text-slate-600">
                                    <span x-text="tag"></span>
                                    <button type="button"
                                        class="text-slate-400 transition hover:text-rose-500 disabled:cursor-not-allowed disabled:opacity-60"
                                        :disabled="tagSaving" @click="removeTag(index)">
                                        <svg class="h-3 w-3" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                            stroke-width="1.6">
                                            <path stroke-linecap="round" stroke-linejoin="round"
                                                d="M6 6l8 8M6 14L14 6" />
                                        </svg>
                                    </button>
                                </span>
                            </template>
                            <template x-if="!(record.tags || []).length">
                                <span class="rounded-full bg-slate-100 px-3 py-1 text-sm text-slate-400">No tags
                                    yet</span>
                            </template>
                        </div>
                        <p class="mt-2 text-xs text-rose-500" x-show="tagError" x-text="tagError"></p>
                        <p class="mt-2 text-xs text-slate-400" x-show="tagSaving">Saving tags…</p>
                    </div>

                    <div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
                        <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                            <div>
                                <h2 class="text-base font-semibold text-slate-900">Call notes</h2>
                                <p class="text-xs text-slate-500">Document commitments, escalations, or customer
                                    sentiment.</p>
                            </div>
                            <button type="button"
                                class="inline-flex items-center gap-2 rounded-lg border border-slate-200 px-3 py-2 text-sm font-semibold text-slate-600 transition hover:border-sky-300 hover:text-sky-700 disabled:cursor-not-allowed disabled:opacity-60"
                                :disabled="noteSaving" @click="saveNotes">
                                <svg class="h-4 w-4" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                    stroke-width="1.6">
                                    <path stroke-linecap="round" stroke-linejoin="round" d="M4 4h12v12H4z" />
                                    <path stroke-linecap="round" stroke-linejoin="round" d="M6 10h8M6 14h5M6 6h8" />
                                </svg>
                                <span x-text="noteSaving ? 'Saving…' : 'Save note'"></span>
                            </button>
                        </div>
                        <textarea x-model="noteDraft" rows="6"
                            class="mt-3 w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200"
                            placeholder="Add internal remark or follow-up task..."></textarea>
                        <p class="mt-2 text-xs text-rose-500" x-show="noteError" x-text="noteError"></p>
                        <p class="mt-1 text-xs text-slate-400" x-show="noteSaving">Saving note…</p>
                        <p class="mt-2 text-xs text-slate-400" x-show="notes.updated_at">
                            Last updated <span x-text="formatDateTime(notes.updated_at)"></span>
                            <span x-show="notes.updated_by">&middot; by <span x-text="notes.updated_by"></span></span>
                        </p>
                    </div>

                    <div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5 lg:col-span-2">
                        <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                            <div>
                                <h2 class="text-base font-semibold text-slate-900">Billing details</h2>
                                <p class="text-xs text-slate-500">Snapshot of increments, rate, and totals at hangup.
                                </p>
                            </div>
                        </div>
                        <div class="mt-4 grid gap-4 sm:grid-cols-2">
                            <dl class="space-y-2 text-sm text-slate-600">
                                <div class="flex items-center justify-between">
                                    <dt>Status</dt>
                                    <dd>
                                        <span
                                            class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-semibold"
                                            :class="billingStatusClasses(billing.summary?.status || record.billing?.status)">
                                            <span
                                                x-text="billingStatusLabel(billing.summary?.status || record.billing?.status)"></span>
                                        </span>
                                    </dd>
                                </div>
                                <div class="flex items-center justify-between">
                                    <dt>Billable seconds</dt>
                                    <dd x-text="billing.summary?.billable_secs ?? record.billing?.billable_secs ?? '—'">
                                    </dd>
                                </div>
                                <div class="flex items-center justify-between">
                                    <dt>Billable minutes</dt>
                                    <dd x-text="formatBillingMinutes(billing.summary || record.billing)"></dd>
                                </div>
                                <div class="flex items-center justify-between">
                                    <dt>Rate / min</dt>
                                    <dd
                                        x-text="billing.summary?.rate_per_minute !== undefined ? formatCurrency(billing.summary.rate_per_minute, billing.summary?.currency || record.billing?.currency) : '—'">
                                    </dd>
                                </div>
                            </dl>
                            <dl class="space-y-2 text-sm text-slate-600">
                                <div class="flex items-center justify-between">
                                    <dt>Subtotal</dt>
                                    <dd
                                        x-text="billing.summary?.amount?.subtotal !== undefined ? formatCurrency(billing.summary.amount.subtotal, billing.summary?.currency || record.billing?.currency) : '—'">
                                    </dd>
                                </div>
                                <div class="flex items-center justify-between">
                                    <dt>Tax</dt>
                                    <dd
                                        x-text="billing.summary?.amount?.tax !== undefined ? formatCurrency(billing.summary.amount.tax, billing.summary?.currency || record.billing?.currency) : '—'">
                                    </dd>
                                </div>
                                <div class="flex items-center justify-between font-semibold">
                                    <dt>Total</dt>
                                    <dd x-text="formatBillingAmount(billing.summary || record.billing)"></dd>
                                </div>
                                <div class="flex items-center justify-between">
                                    <dt>Increments</dt>
                                    <dd
                                        x-text="billing.summary?.increments ? (billing.summary.increments.initial_secs + ' / ' + billing.summary.increments.billing_secs + ' s') : '—'">
                                    </dd>
                                </div>
                            </dl>
                        </div>
                        <template x-if="billing.result">
                            <details
                                class="mt-4 space-y-2 rounded-lg border border-slate-200 bg-slate-50 p-3 text-[11px] text-slate-600">
                                <summary class="cursor-pointer text-xs font-semibold text-slate-500">Computation trace
                                </summary>
                                <pre class="max-h-48 overflow-auto rounded bg-white/70 p-3"
                                    x-text="formatJson(billing.result)"></pre>
                            </details>
                        </template>
                        <template x-if="billing.snapshot">
                            <details
                                class="mt-2 space-y-2 rounded-lg border border-slate-200 bg-slate-50 p-3 text-[11px] text-slate-600">
                                <summary class="cursor-pointer text-xs font-semibold text-slate-500">Template snapshot
                                </summary>
                                <pre class="max-h-48 overflow-auto rounded bg-white/70 p-3"
                                    x-text="formatJson(billing.snapshot)"></pre>
                            </details>
                        </template>
                    </div>
                </div>
            </div>
        </section>
    </div>
</div>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('callRecordDetail', (payload) => ({
            init() {
                const data = typeof payload === 'string' ? JSON.parse(payload || '{}') : (payload || {});
                this.record = data.record || {};
                const recordRewrite = (this.record && this.record.rewrite && typeof this.record.rewrite === 'object')
                    ? this.record.rewrite
                    : null;
                const topLevelRewrite = (data.rewrite && typeof data.rewrite === 'object') ? data.rewrite : null;
                this.rewrite = recordRewrite || topLevelRewrite || {};
                const sipFlowPayload = (data.sip_flow && typeof data.sip_flow === 'object' && !Array.isArray(data.sip_flow))
                    ? data.sip_flow
                    : { entries: Array.isArray(data.sip_flow) ? data.sip_flow : [] };
                const sipFlowEntriesRaw = Array.isArray(sipFlowPayload.entries) ? sipFlowPayload.entries : [];
                this.sipFlowEntries = sipFlowEntriesRaw.map((entry, index) => {
                    const clone = Object.assign({}, entry || {});
                    if (clone.sequence === undefined || clone.sequence === null) {
                        clone.sequence = index;
                    }
                    return clone;
                });
                const payloadColumnLabels = {};
                if (Array.isArray(sipFlowPayload.columns)) {
                    sipFlowPayload.columns.forEach((column) => {
                        if (!column || typeof column !== 'object') {
                            return;
                        }
                        const normalized = this.normalizeLaneId(column.id || column.label);
                        if (normalized && ['time', 'caller', 'pbx', 'callee'].includes(normalized)) {
                            payloadColumnLabels[normalized] = column.label || column.id || payloadColumnLabels[normalized];
                        }
                    });
                }
                this.sipFlowColumns = [
                    { id: 'time', label: payloadColumnLabels.time || 'Time' },
                    { id: 'caller', label: payloadColumnLabels.caller || 'Caller' },
                    { id: 'pbx', label: payloadColumnLabels.pbx || 'PBX' },
                    { id: 'callee', label: payloadColumnLabels.callee || 'Callee' },
                ];
                this.selectedSipEntry = this.sipFlowEntries.length ? this.sipFlowEntries[0] : null;
                const recordStartTs = this.parseTimestamp(this.record.started_at);
                const firstEntryTs = this.sipFlowEntries.length ? this.parseTimestamp(this.sipFlowEntries[0].timestamp) : null;
                this.flowStartEpoch = Number.isFinite(recordStartTs) ? recordStartTs : firstEntryTs;
                this.mediaMetrics = data.media_metrics || {};
                const initialTranscript = (data.transcript && typeof data.transcript === 'object')
                    ? data.transcript
                    : { available: false, segments: [] };
                this.transcriptPreview = data.transcript_preview || null;
                this.transcriptCapabilities = data.transcript_capabilities || {};
                this.actions = data.actions || {};
                this.backUrl = data.back_url || '/console/call-records';
                const fallbackUpdateUrl = this.determineRecordUrl(this.record.id);
                this.updateUrl = (this.actions && this.actions.update_record)
                    ? this.actions.update_record
                    : fallbackUpdateUrl;
                this.transcriptUrl = this.determineTranscriptUrl(
                    this.actions ? this.actions.transcript_url : '',
                    this.record.id,
                );
                this.applyTranscriptPayload({
                    status: initialTranscript?.status || this.record.transcript_status,
                    transcript: initialTranscript,
                }, { resetLanguage: true });
                const initialNotes = (data.notes && typeof data.notes === 'object') ? data.notes : {};
                this.notes = {
                    text: initialNotes.text || '',
                    updated_at: initialNotes.updated_at || null,
                    updated_by: initialNotes.updated_by || null,
                };
                this.noteDraft = this.notes.text || '';
                this.tagError = '';
                this.noteError = '';
                this.participants = Array.isArray(data.participants) ? data.participants : [];
                this.billing = data.billing || {
                    summary: (this.record && this.record.billing) || {},
                    snapshot: null,
                    result: null,
                };
                if (!this.record.billing && this.billing && this.billing.summary) {
                    this.record.billing = this.billing.summary;
                }
                this.durationHint = this.record.ended_at ? `Ended ${this.formatDateTime(this.record.ended_at)}` : 'No hangup timestamp recorded';
                this.detailTab = 'overview';
                if (this.record.has_transcript && !this.transcriptVisible && this.transcriptUrl) {
                    this.fetchTranscript();
                }
            },
            record: {},
            sipFlowEntries: [],
            sipFlowColumns: [],
            selectedSipEntry: null,
            flowStartEpoch: null,
            mediaMetrics: {},
            transcript: {},
            transcriptPreview: null,
            transcriptCapabilities: {},
            transcriptVisible: false,
            transcriptStatus: 'pending',
            transcriptError: null,
            transcriptLoading: false,
            transcriptUrl: '',
            updateUrl: '',
            notes: {},
            noteDraft: '',
            participants: [],
            rewrite: {},
            actions: {},
            backUrl: '/console/call-records',
            billing: {},
            tagInput: '',
            tagSaving: false,
            tagError: '',
            noteSaving: false,
            noteError: '',
            asrProcessing: false,
            durationHint: '',
            detailTab: 'overview',
            copiedCallId: null,
            copyTimer: null,
            selectedLanguage: 'auto',
            languageOptions: [],
            languageSelectionDirty: false,
            async addTag() {
                const value = (this.tagInput || '').trim();
                if (!value) {
                    return;
                }
                const current = Array.isArray(this.record.tags) ? [...this.record.tags] : [];
                if (current.includes(value)) {
                    this.tagInput = '';
                    return;
                }
                const next = [...current, value];
                this.record.tags = next;
                this.tagInput = '';
                this.tagError = '';
                this.tagSaving = true;
                try {
                    const payload = await this.updateRecord({ tags: next });
                    this.applyUpdatePayload(payload, { syncNoteDraft: false });
                } catch (err) {
                    console.error('Failed to add tag', err);
                    this.record.tags = current;
                    this.tagError = (err && err.message) ? err.message : 'Failed to update tags';
                } finally {
                    this.tagSaving = false;
                }
            },
            async removeTag(index) {
                const current = Array.isArray(this.record.tags) ? [...this.record.tags] : [];
                if (index < 0 || index >= current.length) {
                    return;
                }
                const next = [...current];
                next.splice(index, 1);
                this.record.tags = next;
                this.tagError = '';
                this.tagSaving = true;
                try {
                    const payload = await this.updateRecord({ tags: next });
                    this.applyUpdatePayload(payload, { syncNoteDraft: false });
                } catch (err) {
                    console.error('Failed to remove tag', err);
                    this.record.tags = current;
                    this.tagError = (err && err.message) ? err.message : 'Failed to update tags';
                } finally {
                    this.tagSaving = false;
                }
            },
            async saveNotes() {
                const text = this.noteDraft || '';
                const previousNotes = {
                    text: (this.notes && this.notes.text) || '',
                    updated_at: (this.notes && this.notes.updated_at) || null,
                    updated_by: (this.notes && this.notes.updated_by) || null,
                };
                this.noteError = '';
                this.noteSaving = true;
                try {
                    const payload = await this.updateRecord({ note: { text } });
                    this.applyUpdatePayload(payload);
                } catch (err) {
                    console.error('Failed to save note', err);
                    this.noteError = (err && err.message) ? err.message : 'Failed to save note';
                    this.notes = previousNotes;
                } finally {
                    this.noteSaving = false;
                }
            },
            applyUpdatePayload(payload = {}, options = {}) {
                const syncNoteDraft = options.syncNoteDraft !== false;
                if (payload && payload.record && Array.isArray(payload.record.tags)) {
                    this.record.tags = payload.record.tags;
                }
                if (payload && Object.prototype.hasOwnProperty.call(payload, 'notes')) {
                    if (payload.notes && typeof payload.notes === 'object') {
                        this.notes = {
                            text: payload.notes.text || '',
                            updated_at: payload.notes.updated_at || null,
                            updated_by: payload.notes.updated_by || null,
                        };
                    } else {
                        this.notes = { text: '', updated_at: null, updated_by: null };
                    }
                    if (syncNoteDraft) {
                        this.noteDraft = this.notes.text || '';
                    }
                }
            },
            updateSelectedLanguage(value) {
                const normalized = this.normalizeLanguage(value) || 'auto';
                this.selectedLanguage = normalized;
                this.languageSelectionDirty = true;
                if (this.record) {
                    this.record.transcript_language = normalized;
                }
                this.languageOptions = this.buildLanguageOptions();
            },
            buildLanguageOptions() {
                const seen = new Set();
                const order = [];
                const push = (input) => {
                    const normalized = this.normalizeLanguage(input);
                    if (!normalized) {
                        return;
                    }
                    if (seen.has(normalized)) {
                        return;
                    }
                    seen.add(normalized);
                    order.push(normalized);
                };
                push('auto');
                const defaults = ['en', 'zh', 'yue', 'ja', 'ko'];
                defaults.forEach(push);
                const capabilityLanguages = this.transcriptCapabilities?.sensevoice_cli?.languages;
                if (Array.isArray(capabilityLanguages)) {
                    capabilityLanguages.forEach(push);
                }
                push(this.record?.transcript_language);
                push(this.transcript?.language);
                push(this.selectedLanguage);
                return order.map((value) => ({
                    value,
                    label: this.languageLabel(value),
                }));
            },
            normalizeLanguage(value) {
                if (value === null || value === undefined) {
                    return '';
                }
                if (typeof value === 'object') {
                    return '';
                }
                const text = String(value).trim();
                if (!text) {
                    return '';
                }
                return text.toLowerCase();
            },
            languageLabel(value) {
                const normalized = this.normalizeLanguage(value);
                if (!normalized) {
                    return '—';
                }
                const labels = {
                    auto: 'Auto detect',
                    en: 'English (en)',
                    zh: 'Chinese (zh)',
                    yue: 'Cantonese (yue)',
                    ja: 'Japanese (ja)',
                    ko: 'Korean (ko)',
                    es: 'Spanish (es)',
                    fr: 'French (fr)',
                    de: 'German (de)',
                    pt: 'Portuguese (pt)',
                    ru: 'Russian (ru)',
                    vi: 'Vietnamese (vi)',
                    th: 'Thai (th)',
                    id: 'Indonesian (id)',
                };
                if (labels[normalized]) {
                    return labels[normalized];
                }
                return normalized.toUpperCase();
            },
            transcriptGeneratedLabel() {
                if (!(this.transcript?.available || this.transcriptVisible)) {
                    return 'Request on demand to analyse conversation outcome.';
                }
                const formatted = this.formatGeneratedTimestamp(this.transcript?.generated_at);
                if (formatted) {
                    return formatted;
                }
                return 'Transcript ready.';
            },
            canRequestTranscript() {
                const capability = this.transcriptCapabilities && this.transcriptCapabilities.sensevoice_cli;
                if (capability && capability.ready) {
                    return true;
                }
                return Boolean(this.record && this.record.has_transcript);
            },
            transcriptCapabilityIssues() {
                const capability = this.transcriptCapabilities && this.transcriptCapabilities.sensevoice_cli;
                if (!capability) {
                    return [];
                }
                if (Array.isArray(capability.missing)) {
                    return capability.missing;
                }
                return [];
            },
            transcriptTimeline() {
                const segments = Array.isArray(this.transcript?.segments) ? this.transcript.segments : [];
                if (!segments.length) {
                    return [];
                }
                const enriched = segments.map((segment, index) => {
                    const startValue = Number(segment?.start);
                    const endValue = Number(segment?.end);
                    const rawChannel = segment ? segment.channel : null;
                    let channelKey = 'mono';
                    if (rawChannel !== null && rawChannel !== undefined && rawChannel !== '') {
                        const numericChannel = Number(rawChannel);
                        channelKey = Number.isFinite(numericChannel) ? numericChannel : 'mono';
                    }
                    let side = 'mono';
                    if (channelKey !== 'mono') {
                        const numeric = Number(channelKey);
                        if (Number.isFinite(numeric)) {
                            if (numeric === 0) {
                                side = 'left';
                            } else if (numeric === 1) {
                                side = 'right';
                            } else {
                                side = numeric % 2 === 0 ? 'left' : 'right';
                            }
                        }
                    }
                    const label = segment?.speaker || this.channelLabel(channelKey);
                    const start = Number.isFinite(startValue) ? startValue : null;
                    const end = Number.isFinite(endValue) ? endValue : null;
                    return {
                        key: `timeline-${index}-${segment?.idx ?? index}-${start ?? ''}`,
                        segment,
                        side,
                        label,
                        start,
                        end,
                    };
                });
                enriched.sort((a, b) => {
                    if (a.start === null && b.start === null) {
                        return 0;
                    }
                    if (a.start === null) {
                        return 1;
                    }
                    if (b.start === null) {
                        return -1;
                    }
                    if (a.start === b.start) {
                        return 0;
                    }
                    return a.start - b.start;
                });
                return enriched;
            },
            transcriptAlignmentClass(side) {
                if (side === 'right') {
                    return 'justify-end';
                }
                if (side === 'mono') {
                    return 'justify-center';
                }
                return 'justify-start';
            },
            transcriptCardTone(side) {
                if (side === 'right') {
                    return 'border-emerald-200 bg-emerald-50/80';
                }
                if (side === 'left') {
                    return 'border-sky-200 bg-sky-50/80';
                }
                return 'border-slate-200 bg-white/90';
            },
            channelLabel(key) {
                if (key === 'mono' || key === null || key === undefined) {
                    return 'Mono channel';
                }
                const numeric = Number(key);
                if (!Number.isFinite(numeric)) {
                    return `Channel ${key}`;
                }
                if (numeric === 0) {
                    return 'Left channel';
                }
                if (numeric === 1) {
                    return 'Right channel';
                }
                return `Channel ${numeric + 1}`;
            },
            formatSegmentTimestamp(value) {
                if (value === undefined || value === null) {
                    return '';
                }
                const numeric = Number(value);
                if (!Number.isFinite(numeric)) {
                    return String(value);
                }
                return `${numeric.toFixed(1)}s`;
            },
            formatSegmentRange(start, end) {
                const startText = this.formatSegmentTimestamp(start);
                const endText = this.formatSegmentTimestamp(end);
                const hasStart = Boolean(startText);
                const hasEnd = end !== undefined && end !== null && end !== '' && Boolean(endText);
                if (hasStart && hasEnd) {
                    if (startText === endText) {
                        return startText;
                    }
                    return `${startText} → ${endText}`;
                }
                if (!hasStart && hasEnd) {
                    return endText;
                }
                return startText;
            },
            async requestTranscript(force = false) {
                if (!this.transcriptUrl) {
                    this.transcriptError = 'Transcript endpoint unavailable.';
                    return;
                }
                if (this.asrProcessing || this.transcriptLoading) {
                    return;
                }
                if (!force && this.record?.has_transcript && !this.transcriptVisible) {
                    await this.fetchTranscript();
                    return;
                }
                if (!force && this.transcriptStatus === 'processing') {
                    await this.fetchTranscript();
                    return;
                }
                this.asrProcessing = true;
                this.transcriptError = null;
                this.transcriptStatus = 'processing';
                if (this.record) {
                    this.record.transcript_status = 'processing';
                }
                const payload = {};
                if (force) {
                    payload.force = true;
                }
                let selectedLanguage = this.normalizeLanguage(this.selectedLanguage);
                if (!selectedLanguage) {
                    selectedLanguage = this.normalizeLanguage(
                        this.record?.transcript_language || this.transcript?.language,
                    );
                }
                if (selectedLanguage && selectedLanguage !== 'auto') {
                    payload.language = selectedLanguage;
                }
                try {
                    const response = await fetch(this.transcriptUrl, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'Accept': 'application/json',
                        },
                        body: JSON.stringify(payload),
                    });
                    if (!response.ok) {
                        this.transcriptError = await this.extractErrorMessage(response);
                        if (response.status === 409) {
                            await this.fetchTranscript();
                        }
                        return;
                    }
                    const data = await response.json();
                    this.applyTranscriptPayload(data);
                    if (data && data.transcript) {
                        this.transcriptPreview = data.transcript;
                    }
                } catch (err) {
                    console.error('Failed to request transcript', err);
                    this.transcriptError = 'Failed to request transcription';
                } finally {
                    this.asrProcessing = false;
                }
            },
            async fetchTranscript() {
                if (!this.transcriptUrl) {
                    this.transcriptError = 'Transcript endpoint unavailable.';
                    return;
                }
                if (this.transcriptLoading || this.asrProcessing) {
                    return;
                }
                this.transcriptLoading = true;
                this.transcriptError = null;
                try {
                    const response = await fetch(this.transcriptUrl, {
                        headers: { 'Accept': 'application/json' },
                    });
                    if (response.status === 404) {
                        this.applyTranscriptPayload({ status: 'pending', transcript: { available: false } });
                        return;
                    }
                    if (!response.ok) {
                        this.transcriptError = await this.extractErrorMessage(response);
                        return;
                    }
                    const data = await response.json();
                    this.applyTranscriptPayload(data);
                } catch (err) {
                    console.error('Failed to load transcript', err);
                    this.transcriptError = 'Failed to load transcript';
                } finally {
                    this.transcriptLoading = false;
                }
            },
            applyTranscriptPayload(payload = {}, options = {}) {
                const { resetLanguage = false } = options;
                const previousTranscript = this.transcript || {};
                const previousGeneratedAt = previousTranscript?.generated_at || null;
                const previousRecordLanguage = this.normalizeLanguage(this.record?.transcript_language);
                const transcript = (payload.transcript && typeof payload.transcript === 'object')
                    ? payload.transcript
                    : { available: false, segments: [] };
                const normalizedRaw = (payload.status || transcript.status || this.record?.transcript_status || 'pending')
                    .toString()
                    .toLowerCase();
                const normalized = normalizedRaw || 'pending';
                this.transcript = transcript;
                this.transcriptVisible = Boolean(transcript.available);
                this.transcriptStatus = normalized;
                if (this.record) {
                    this.record.transcript_status = normalized;
                    if (typeof transcript.available === 'boolean') {
                        this.record.has_transcript = transcript.available;
                    }
                }
                const currentGeneratedAt = transcript?.generated_at || null;
                const isNewTranscript = Boolean(
                    currentGeneratedAt && currentGeneratedAt !== previousGeneratedAt,
                );
                const candidateLanguage = this.normalizeLanguage(transcript.language)
                    || previousRecordLanguage
                    || this.normalizeLanguage(this.selectedLanguage)
                    || 'auto';
                const shouldSyncLanguage = resetLanguage
                    || !this.selectedLanguage
                    || this.selectedLanguage === ''
                    || isNewTranscript
                    || (!this.languageSelectionDirty
                        && (transcript.available || normalized === 'completed'));
                if (shouldSyncLanguage) {
                    this.selectedLanguage = candidateLanguage || 'auto';
                    this.languageSelectionDirty = false;
                }
                if (this.record) {
                    this.record.transcript_language = this.selectedLanguage || 'auto';
                }
                this.languageOptions = this.buildLanguageOptions();
            },
            async updateRecord(update) {
                if (!update || typeof update !== 'object') {
                    throw new Error('No update payload provided.');
                }
                if (!this.updateUrl) {
                    throw new Error('Update endpoint unavailable.');
                }
                try {
                    const response = await fetch(this.updateUrl, {
                        method: 'PATCH',
                        headers: {
                            'Content-Type': 'application/json',
                            'Accept': 'application/json',
                        },
                        body: JSON.stringify(update),
                    });
                    if (!response.ok) {
                        throw new Error(await this.extractErrorMessage(response));
                    }
                    return await response.json();
                } catch (err) {
                    if (err instanceof Error) {
                        throw err;
                    }
                    throw new Error('Failed to update call record');
                }
            },
            async extractErrorMessage(response) {
                let fallback = `Request failed (${response.status})`;
                const type = response.headers.get('content-type') || '';
                if (type.includes('application/json')) {
                    try {
                        const data = await response.json();
                        if (data && typeof data === 'object' && data.message) {
                            return data.message;
                        }
                        if (data && typeof data === 'string') {
                            return data;
                        }
                    } catch (err) {
                        return fallback;
                    }
                    return fallback;
                }
                try {
                    const text = await response.text();
                    if (text) {
                        return text;
                    }
                } catch (err) {
                    return fallback;
                }
                return fallback;
            },
            determineTranscriptUrl(hint, recordId) {
                const hinted = this.normalizeConsoleUrl(hint);
                if (hinted) {
                    return hinted;
                }
                if (!recordId) {
                    return '';
                }
                const base = this.normalizeConsoleUrl(this.backUrl || '/console/call-records');
                const trimmed = base.endsWith('/') ? base.slice(0, -1) : base;
                return this.normalizeConsoleUrl(`${trimmed}/${recordId}/transcript`);
            },
            determineRecordUrl(recordId) {
                if (!recordId) {
                    return '';
                }
                const base = this.normalizeConsoleUrl(this.backUrl || '/console/call-records');
                const trimmed = base.endsWith('/') ? base.slice(0, -1) : base;
                return this.normalizeConsoleUrl(`${trimmed}/${encodeURIComponent(recordId)}`);
            },
            normalizeConsoleUrl(value) {
                const candidate = (value || '').toString().trim();
                if (!candidate) {
                    return '';
                }
                if (candidate.startsWith('http://') || candidate.startsWith('https://')) {
                    try {
                        const parsed = new URL(candidate, window.location.origin);
                        return parsed.pathname + parsed.search;
                    } catch (err) {
                        return candidate;
                    }
                }
                return candidate.startsWith('/') ? candidate : `/${candidate}`;
            },
            selectSipEntry(entry) {
                if (!entry) {
                    this.selectedSipEntry = null;
                    return;
                }
                if (this.selectedSipEntry && this.selectedSipEntry.sequence === entry.sequence) {
                    this.selectedSipEntry = null;
                } else {
                    this.selectedSipEntry = entry;
                }
            },
            laneCell(entry, columnId) {
                if (!entry || !columnId) {
                    return 'none';
                }
                const target = this.normalizeLaneId(columnId);
                if (!target) {
                    return 'none';
                }
                const laneFrom = this.normalizeLaneId(entry.lane_from);
                const laneTo = this.normalizeLaneId(entry.lane_to);
                if (laneFrom === target && laneTo === target) {
                    return 'self';
                }
                if (laneFrom === target) {
                    return 'from';
                }
                if (laneTo === target) {
                    return 'to';
                }
                return 'none';
            },
            lanePosition(laneId) {
                switch (this.normalizeLaneId(laneId)) {
                    case 'caller':
                        return 0;
                    case 'pbx':
                        return 1;
                    case 'callee':
                        return 2;
                    default:
                        return 1;
                }
            },
            cellArrow(entry, columnId) {
                if (!entry || !columnId) {
                    return '';
                }
                const cellType = this.laneCell(entry, columnId);
                if (cellType !== 'from' && cellType !== 'to') {
                    return '';
                }
                const fromLane = this.normalizeLaneId(entry.lane_from);
                const toLane = this.normalizeLaneId(entry.lane_to);
                if (!fromLane || !toLane || fromLane === toLane) {
                    return '';
                }
                return this.lanePosition(toLane) > this.lanePosition(fromLane) ? '→' : '←';
            },
            arrowPlacement(entry, columnId, cellType) {
                const arrow = this.cellArrow(entry, columnId);
                if (!arrow) {
                    return 'none';
                }
                const lane = this.normalizeLaneId(columnId);
                if (cellType === 'from') {
                    if (lane === 'callee' && arrow === '←') {
                        return 'right';
                    }
                    return arrow === '→' ? 'right' : 'left';
                }
                if (cellType === 'to') {
                    return arrow === '→' ? 'right' : 'left';
                }
                return 'none';
            },
            laneContentClass(entry, columnId) {
                const lane = this.normalizeLaneId(columnId);
                const cellType = this.laneCell(entry, columnId);
                if (lane === 'callee') {
                    if (cellType === 'to') {
                        return 'justify-end text-right';
                    }
                    if (cellType === 'from') {
                        return 'text-left';
                    }
                }
                if (cellType === 'to') {
                    return 'justify-end text-right';
                }
                if (cellType === 'from') {
                    return 'text-left';
                }
                return 'text-left';
            },
            normalizeLaneId(value) {
                const normalized = (value || '').toString().trim().toLowerCase();
                if (!normalized) {
                    return '';
                }
                switch (normalized) {
                    case 'pbx':
                    case 'proxy':
                    case 'server':
                    case 'sip_server':
                    case 'b2bua':
                        return 'pbx';
                    case 'caller':
                    case 'source':
                    case 'src':
                        return 'caller';
                    case 'callee':
                    case 'destination':
                    case 'dst':
                        return 'callee';
                    case 'time':
                    case 'timestamp':
                        return 'time';
                    default:
                        return normalized;
                }
            },
            parseTimestamp(value) {
                if (!value) {
                    return NaN;
                }
                const date = new Date(value);
                if (!Number.isNaN(date.getTime())) {
                    return date.getTime();
                }
                const parsed = Date.parse(value);
                return Number.isNaN(parsed) ? NaN : parsed;
            },
            formatRelativeToStart(entry) {
                if (!entry) {
                    return '—';
                }
                const timestamp = this.parseTimestamp(entry.timestamp);
                if (!Number.isFinite(timestamp)) {
                    return entry.offset || '—';
                }
                if (!Number.isFinite(this.flowStartEpoch)) {
                    return entry.offset || '—';
                }
                return this.formatDiffWithMs(timestamp - this.flowStartEpoch);
            },
            formatDiffWithMs(diffMs) {
                if (!Number.isFinite(diffMs)) {
                    return '—';
                }
                const sign = diffMs < 0 ? '-' : '+';
                const abs = Math.abs(diffMs);
                const totalSeconds = Math.floor(abs / 1000);
                const milliseconds = abs % 1000;
                const hours = Math.floor(totalSeconds / 3600);
                const minutes = Math.floor((totalSeconds % 3600) / 60);
                const seconds = totalSeconds % 60;
                const pad = (num, len = 2) => String(num).padStart(len, '0');
                const base = hours > 0
                    ? `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
                    : `${pad(minutes)}:${pad(seconds)}`;
                return `${sign}${base}.${pad(milliseconds, 3)}`;
            },
            formatDateTimeWithMs(value) {
                const timestamp = this.parseTimestamp(value);
                if (!Number.isFinite(timestamp)) {
                    return value || '—';
                }
                const date = new Date(timestamp);
                const pad = (num, len = 2) => String(num).padStart(len, '0');
                const year = date.getFullYear();
                const month = pad(date.getMonth() + 1);
                const day = pad(date.getDate());
                const hours = pad(date.getHours());
                const minutes = pad(date.getMinutes());
                const seconds = pad(date.getSeconds());
                const milliseconds = pad(date.getMilliseconds(), 3);
                return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
            },
            formatSummary(value) {
                const original = (value || '').toString().trim();
                if (!original) {
                    return '—';
                }
                const compact = original
                    .replace(/sips?:[^\s>]+/gi, '')
                    .replace(/<[^>]*>/g, '')
                    .replace(/"[^"\\]*"/g, '')
                    .replace(/\bSIP\/2\.0\b/gi, '')
                    .replace(/\bOK\b/gi, '')
                    .replace(/\s{2,}/g, ' ')
                    .trim();
                if (!compact) {
                    const firstToken = original.split(/\s+/)[0];
                    return firstToken || '—';
                }
                return compact.length > 60 ? `${compact.slice(0, 57)}…` : compact;
            },
            isB2bua(entry) {
                if (!entry || !entry.leg_role) {
                    return false;
                }
                return entry.leg_role.toLowerCase() === 'b2bua';
            },
            async copyCallId(callId) {
                if (!callId) {
                    return;
                }
                const text = String(callId);
                let ok = await this.tryClipboardWrite(text);
                if (!ok) {
                    ok = this.copyCallIdFallback(text);
                }
                if (ok) {
                    this.showCopyConfirmation(text);
                }
            },
            async tryClipboardWrite(text) {
                if (typeof navigator === 'undefined' || !navigator.clipboard || !navigator.clipboard.writeText) {
                    return false;
                }
                try {
                    await navigator.clipboard.writeText(text);
                    return true;
                } catch (err) {
                    console.error('Clipboard API failed', err);
                    return false;
                }
            },
            copyCallIdFallback(text) {
                try {
                    const temp = document.createElement('textarea');
                    temp.value = text;
                    temp.setAttribute('readonly', '');
                    temp.style.position = 'fixed';
                    temp.style.opacity = '0';
                    document.body.appendChild(temp);
                    temp.select();
                    temp.setSelectionRange(0, temp.value.length);
                    const success = document.execCommand('copy');
                    document.body.removeChild(temp);
                    return success;
                } catch (err) {
                    console.error('Copy fallback failed', err);
                    return false;
                }
            },
            showCopyConfirmation(value) {
                this.copiedCallId = value;
                if (this.copyTimer) {
                    clearTimeout(this.copyTimer);
                }
                this.copyTimer = setTimeout(() => {
                    this.copiedCallId = null;
                    this.copyTimer = null;
                }, 1500);
            },
            statusLabel(value) {
                switch ((value || '').toLowerCase()) {
                    case 'completed':
                        return 'Completed';
                    case 'missed':
                        return 'Missed';
                    case 'failed':
                        return 'Failed';
                    default:
                        return value || 'Unknown';
                }
            },
            directionLabel(value) {
                switch ((value || '').toLowerCase()) {
                    case 'inbound':
                        return 'Inbound';
                    case 'outbound':
                        return 'Outbound';
                    case 'internal':
                        return 'Internal';
                    default:
                        return value || 'Unknown';
                }
            },
            billingStatusLabel(value) {
                switch ((value || '').toLowerCase()) {
                    case 'charged':
                        return 'Charged';
                    case 'included':
                        return 'Included';
                    case 'zero-duration':
                        return 'Zero duration';
                    case 'unrated':
                        return 'Unrated';
                    default:
                        return value || 'Unknown';
                }
            },
            statusClasses(status) {
                switch ((status || '').toLowerCase()) {
                    case 'completed':
                        return 'bg-emerald-50 text-emerald-600 ring-1 ring-emerald-200';
                    case 'missed':
                        return 'bg-amber-50 text-amber-600 ring-1 ring-amber-200';
                    case 'failed':
                        return 'bg-rose-50 text-rose-600 ring-1 ring-rose-200';
                    default:
                        return 'bg-slate-100 text-slate-600 ring-1 ring-slate-200';
                }
            },
            billingStatusClasses(status) {
                switch ((status || '').toLowerCase()) {
                    case 'charged':
                        return 'bg-emerald-50 text-emerald-600 ring-1 ring-emerald-200';
                    case 'included':
                        return 'bg-sky-50 text-sky-600 ring-1 ring-sky-200';
                    case 'zero-duration':
                        return 'bg-slate-100 text-slate-600 ring-1 ring-slate-200';
                    case 'unrated':
                        return 'bg-amber-50 text-amber-600 ring-1 ring-amber-200';
                    default:
                        return 'bg-slate-100 text-slate-600 ring-1 ring-slate-200';
                }
            },
            statusDot(status) {
                switch ((status || '').toLowerCase()) {
                    case 'completed':
                        return 'bg-emerald-500';
                    case 'missed':
                        return 'bg-amber-500';
                    case 'failed':
                        return 'bg-rose-500';
                    default:
                        return 'bg-slate-400';
                }
            },
            directionClasses(direction) {
                switch ((direction || '').toLowerCase()) {
                    case 'inbound':
                        return 'border border-sky-100 bg-sky-50 text-sky-700';
                    case 'outbound':
                        return 'border border-emerald-100 bg-emerald-50 text-emerald-700';
                    case 'internal':
                        return 'border border-slate-200 bg-slate-50 text-slate-700';
                    default:
                        return 'border border-slate-200 bg-slate-50 text-slate-700';
                }
            },
            formatCurrency(amount, currency) {
                if (amount === null || amount === undefined) {
                    return '—';
                }
                const numeric = Number(amount);
                if (!Number.isFinite(numeric)) {
                    return '—';
                }
                const symbol = (currency || 'USD').toString().trim().toUpperCase() || 'USD';
                return `${symbol} ${numeric.toLocaleString(undefined, {
                    minimumFractionDigits: 2,
                    maximumFractionDigits: 2,
                })}`;
            },
            formatBillingAmount(billing) {
                if (!billing || typeof billing !== 'object') {
                    return '—';
                }
                const status = (billing.status || '').toLowerCase();
                const amount = billing.amount || {};
                const total = amount.total ?? amount.subtotal ?? null;
                if (total !== null && total !== undefined) {
                    return this.formatCurrency(total, billing.currency || (this.billing?.summary?.currency) || 'USD');
                }
                if (status === 'included') {
                    return 'Included';
                }
                if (status === 'zero-duration') {
                    return '0.00';
                }
                if (status === 'unrated') {
                    return 'Unrated';
                }
                return '—';
            },
            formatBillingMinutes(billing) {
                if (!billing || typeof billing !== 'object') {
                    return '—';
                }
                const minutes = billing.billable_minutes !== undefined
                    ? Number(billing.billable_minutes)
                    : (typeof billing.billable_secs === 'number'
                        ? Number(billing.billable_secs) / 60.0
                        : NaN);
                if (!Number.isFinite(minutes) || minutes <= 0) {
                    return '—';
                }
                return `${minutes.toLocaleString(undefined, {
                    minimumFractionDigits: 2,
                    maximumFractionDigits: 2,
                })} min`;
            },
            qualityTone(value) {
                const mos = Number(value || 0);
                if (mos >= 4.3) {
                    return 'text-emerald-600';
                }
                if (mos >= 3.8) {
                    return 'text-amber-600';
                }
                if (mos > 0) {
                    return 'text-rose-600';
                }
                return 'text-slate-400';
            },
            transcriptStatusTone(status) {
                switch ((status || '').toLowerCase()) {
                    case 'completed':
                        return 'text-emerald-600';
                    case 'processing':
                        return 'text-amber-500';
                    case 'failed':
                        return 'text-rose-500';
                    default:
                        return 'text-slate-400';
                }
            },
            transcriptStatusLabel(status) {
                switch ((status || '').toLowerCase()) {
                    case 'completed':
                        return 'Ready';
                    case 'processing':
                        return 'Processing';
                    case 'failed':
                        return 'Failed';
                    default:
                        return 'Pending';
                }
            },
            transcriptStatusDescription() {
                const status = (this.transcriptStatus || '').toLowerCase();
                if (status === 'completed') {
                    if (this.transcript && this.transcript.generated_at) {
                        return `Generated ${this.formatDateTime(this.transcript.generated_at)}`;
                    }
                    return 'Transcript ready.';
                }
                if (status === 'processing') {
                    return 'Transcription in progress. Refresh to check status.';
                }
                if (status === 'failed') {
                    return 'Previous transcription failed. Retry to run again.';
                }
                if (!this.canRequestTranscript()) {
                    const reasons = this.transcriptCapabilityIssues();
                    if (reasons.length) {
                        return reasons[0];
                    }
                    return 'SenseVoice CLI is not ready.';
                }
                return 'Request on demand to analyse conversation outcome.';
            },
            transcriptStatusIcon(status, hasTranscript = false, visible = false) {
                const value = (status || '').toLowerCase();
                if (value === 'completed' || hasTranscript || visible) {
                    return 'ready';
                }
                if (value === 'processing') {
                    return 'processing';
                }
                if (value === 'failed') {
                    return 'failed';
                }
                return 'pending';
            },
            transcriptButtonLabel() {
                if (this.asrProcessing) {
                    return 'Submitting…';
                }
                if (this.transcriptLoading) {
                    return 'Loading…';
                }
                const status = (this.transcriptStatus || '').toLowerCase();
                if (status === 'processing') {
                    return 'Refresh status';
                }
                if (status === 'failed') {
                    return 'Retry transcript';
                }
                if (this.transcriptVisible) {
                    return 'Re-run transcript';
                }
                if (this.record && this.record.has_transcript) {
                    return 'Load transcript';
                }
                return 'Request transcript';
            },
            formatNumber(value) {
                if (value === null || value === undefined || Number.isNaN(Number(value))) {
                    return '—';
                }
                return Number(value).toLocaleString(undefined, { maximumFractionDigits: 2 });
            },
            formatDuration(value) {
                const seconds = Number(value || 0);
                if (!seconds) {
                    return '00:00';
                }
                const hours = Math.floor(seconds / 3600);
                const minutes = Math.floor((seconds % 3600) / 60);
                const secs = Math.floor(seconds % 60);
                const pad = (num) => String(num).padStart(2, '0');
                if (hours > 0) {
                    return `${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
                }
                return `${pad(minutes)}:${pad(secs)}`;
            },
            formatGeneratedTimestamp(value) {
                if (!value) {
                    return '';
                }
                const formatted = this.formatDateTime(value);
                if (!formatted || formatted === '—') {
                    return '';
                }
                return `Generated ${formatted}`;
            },
            formatDateTime(value) {
                if (!value) {
                    return '—';
                }
                try {
                    let input = value;
                    if (typeof input === 'string') {
                        input = input.trim();
                    }
                    if (!input) {
                        return '—';
                    }
                    let date = new Date(input);
                    if (Number.isNaN(date.getTime()) && typeof input === 'string') {
                        const normalized = input.replace(
                            /(T\d{2}:\d{2}:\d{2}\.)(\d+)(Z|[+-]\d{2}:\d{2})$/,
                            (_, prefix, fraction, suffix) => `${prefix}${fraction.slice(0, 3)}${suffix}`,
                        );
                        if (normalized !== input) {
                            date = new Date(normalized);
                        }
                    }
                    if (Number.isNaN(date.getTime())) {
                        return typeof input === 'string' ? input : '—';
                    }
                    return new Intl.DateTimeFormat(undefined, {
                        year: 'numeric',
                        month: '2-digit',
                        day: '2-digit',
                        hour: '2-digit',
                        minute: '2-digit',
                        second: '2-digit',
                        hour12: false,
                    }).format(date);
                } catch (err) {
                    if (typeof value === 'string') {
                        return value;
                    }
                    return '—';
                }
            },
            formatJson(value) {
                if (!value) {
                    return '—';
                }
                try {
                    return JSON.stringify(value, null, 2);
                } catch (err) {
                    return String(value);
                }
            },
        }));
    });
</script>
{% endblock %}